Passed
Push — master ( 445067...6ce435 )
by Johan
02:18
created

Lexer.js ➔ validate   F

Complexity

Conditions 39

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 21
rs 0
c 0
b 0
f 0
cc 39

How to fix   Complexity   

Complexity

Complex classes like Lexer.js ➔ validate often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
var SyntaxReferenceError = require('./error').SyntaxReferenceError;
2
var MatchError = require('./error').MatchError;
3
var names = require('../utils/names');
4
var generic = require('./generic');
5
var parse = require('../definition-syntax/parse');
6
var generate = require('../definition-syntax/generate');
7
var walk = require('../definition-syntax/walk');
8
var prepareTokens = require('./prepare-tokens');
9
var buildMatchGraph = require('./match-graph').buildMatchGraph;
10
var matchAsTree = require('./match').matchAsTree;
11
var trace = require('./trace');
12
var search = require('./search');
13
var getStructureFromConfig = require('./structure').getStructureFromConfig;
14
var cssWideKeywords = buildMatchGraph('inherit | initial | unset');
15
var cssWideKeywordsWithExpression = buildMatchGraph('inherit | initial | unset | <-ms-legacy-expression>');
16
17
function dumpMapSyntax(map, compact, syntaxAsAst) {
18
    var result = {};
19
20
    for (var name in map) {
21
        if (map[name].syntax) {
22
            result[name] = syntaxAsAst
23
                ? map[name].syntax
24
                : generate(map[name].syntax, { compact: compact });
25
        }
26
    }
27
28
    return result;
29
}
30
31
function valueHasVar(tokens) {
32
    for (var i = 0; i < tokens.length; i++) {
33
        if (tokens[i].value.toLowerCase() === 'var(') {
34
            return true;
35
        }
36
    }
37
38
    return false;
39
}
40
41
function buildMatchResult(match, error, iterations) {
42
    return {
43
        matched: match,
44
        iterations: iterations,
45
        error: error,
46
        getTrace: trace.getTrace,
47
        isType: trace.isType,
48
        isProperty: trace.isProperty,
49
        isKeyword: trace.isKeyword
50
    };
51
}
52
53
function matchSyntax(lexer, syntax, value, useCommon) {
54
    var tokens = prepareTokens(value, lexer.syntax);
55
    var result;
56
57
    if (valueHasVar(tokens)) {
58
        return buildMatchResult(null, new Error('Matching for a tree with var() is not supported'));
59
    }
60
61
    if (useCommon) {
62
        result = matchAsTree(tokens, lexer.valueCommonSyntax, lexer);
63
    }
64
65
    if (!useCommon || !result.match) {
66
        result = matchAsTree(tokens, syntax.match, lexer);
67
        if (!result.match) {
68
            return buildMatchResult(
69
                null,
70
                new MatchError(result.reason, syntax.syntax, value, result),
71
                result.iterations
72
            );
73
        }
74
    }
75
76
    return buildMatchResult(result.match, null, result.iterations);
77
}
78
79
var Lexer = function(config, syntax, structure) {
80
    this.valueCommonSyntax = cssWideKeywords;
81
    this.syntax = syntax;
82
    this.generic = false;
83
    this.properties = {};
84
    this.types = {};
85
    this.structure = structure || getStructureFromConfig(config);
86
87
    if (config) {
88
        if (config.types) {
89
            for (var name in config.types) {
90
                this.addType_(name, config.types[name]);
91
            }
92
        }
93
94
        if (config.generic) {
95
            this.generic = true;
96
            for (var name in generic) {
97
                this.addType_(name, generic[name]);
98
            }
99
        }
100
101
        if (config.properties) {
102
            for (var name in config.properties) {
103
                this.addProperty_(name, config.properties[name]);
104
            }
105
        }
106
    }
107
};
108
109
Lexer.prototype = {
110
    structure: {},
111
    checkStructure: function(ast) {
112
        function collectWarning(node, message) {
113
            warns.push({
114
                node: node,
115
                message: message
116
            });
117
        }
118
119
        var structure = this.structure;
120
        var warns = [];
121
122
        this.syntax.walk(ast, function(node) {
123
            if (structure.hasOwnProperty(node.type)) {
124
                structure[node.type].check(node, collectWarning);
125
            } else {
126
                collectWarning(node, 'Unknown node type `' + node.type + '`');
127
            }
128
        });
129
130
        return warns.length ? warns : false;
131
    },
132
133
    createDescriptor: function(syntax, type, name) {
134
        var ref = {
135
            type: type,
136
            name: name
137
        };
138
        var descriptor = {
139
            type: type,
140
            name: name,
141
            syntax: null,
142
            match: null
143
        };
144
145
        if (typeof syntax === 'function') {
146
            descriptor.match = buildMatchGraph(syntax, ref);
147
        } else {
148
            if (typeof syntax === 'string') {
149
                // lazy parsing on first access
150
                Object.defineProperty(descriptor, 'syntax', {
151
                    get: function() {
152
                        Object.defineProperty(descriptor, 'syntax', {
153
                            value: parse(syntax)
154
                        });
155
156
                        return descriptor.syntax;
157
                    }
158
                });
159
            } else {
160
                descriptor.syntax = syntax;
161
            }
162
163
            // lazy graph build on first access
164
            Object.defineProperty(descriptor, 'match', {
165
                get: function() {
166
                    Object.defineProperty(descriptor, 'match', {
167
                        value: buildMatchGraph(descriptor.syntax, ref)
168
                    });
169
170
                    return descriptor.match;
171
                }
172
            });
173
        }
174
175
        return descriptor;
176
    },
177
    addProperty_: function(name, syntax) {
178
        this.properties[name] = this.createDescriptor(syntax, 'Property', name);
179
    },
180
    addType_: function(name, syntax) {
181
        this.types[name] = this.createDescriptor(syntax, 'Type', name);
182
183
        if (syntax === generic['-ms-legacy-expression']) {
184
            this.valueCommonSyntax = cssWideKeywordsWithExpression;
185
        }
186
    },
187
188
    matchDeclaration: function(node) {
189
        if (node.type !== 'Declaration') {
190
            return buildMatchResult(null, new Error('Not a Declaration node'));
191
        }
192
193
        return this.matchProperty(node.property, node.value);
194
    },
195
    matchProperty: function(propertyName, value) {
196
        var property = names.property(propertyName);
197
198
        // don't match syntax for a custom property
199
        if (property.custom) {
200
            return buildMatchResult(null, new Error('Lexer matching doesn\'t applicable for custom properties'));
201
        }
202
203
        var propertySyntax = property.vendor
204
            ? this.getProperty(property.name) || this.getProperty(property.basename)
205
            : this.getProperty(property.name);
206
207
        if (!propertySyntax) {
208
            return buildMatchResult(null, new SyntaxReferenceError('Unknown property', propertyName));
209
        }
210
211
        return matchSyntax(this, propertySyntax, value, true);
212
    },
213
    matchType: function(typeName, value) {
214
        var typeSyntax = this.getType(typeName);
215
216
        if (!typeSyntax) {
217
            return buildMatchResult(null, new SyntaxReferenceError('Unknown type', typeName));
218
        }
219
220
        return matchSyntax(this, typeSyntax, value, false);
221
    },
222
    match: function(syntax, value) {
223
        if (typeof syntax !== 'string' && (!syntax || !syntax.type)) {
224
            return buildMatchResult(null, new SyntaxReferenceError('Bad syntax'));
225
        }
226
227
        if (typeof syntax === 'string' || !syntax.match) {
228
            syntax = this.createDescriptor(syntax, 'Type', 'anonymous');
229
        }
230
231
        return matchSyntax(this, syntax, value, false);
232
    },
233
234
    findValueFragments: function(propertyName, value, type, name) {
235
        return search.matchFragments(this, value, this.matchProperty(propertyName, value), type, name);
236
    },
237
    findDeclarationValueFragments: function(declaration, type, name) {
238
        return search.matchFragments(this, declaration.value, this.matchDeclaration(declaration), type, name);
239
    },
240
    findAllFragments: function(ast, type, name) {
241
        var result = [];
242
243
        this.syntax.walk(ast, {
244
            visit: 'Declaration',
245
            enter: function(declaration) {
246
                result.push.apply(result, this.findDeclarationValueFragments(declaration, type, name));
247
            }.bind(this)
248
        });
249
250
        return result;
251
    },
252
253
    getProperty: function(name) {
254
        return this.properties.hasOwnProperty(name) ? this.properties[name] : null;
255
    },
256
    getType: function(name) {
257
        return this.types.hasOwnProperty(name) ? this.types[name] : null;
258
    },
259
260
    validate: function() {
261
        function validate(syntax, name, broken, descriptor) {
262
            if (broken.hasOwnProperty(name)) {
263
                return broken[name];
264
            }
265
266
            broken[name] = false;
267
            if (descriptor.syntax !== null) {
268
                walk(descriptor.syntax, function(node) {
269
                    if (node.type !== 'Type' && node.type !== 'Property') {
270
                        return;
271
                    }
272
273
                    var map = node.type === 'Type' ? syntax.types : syntax.properties;
274
                    var brokenMap = node.type === 'Type' ? brokenTypes : brokenProperties;
275
276
                    if (!map.hasOwnProperty(node.name) || validate(syntax, node.name, brokenMap, map[node.name])) {
277
                        broken[name] = true;
278
                    }
279
                }, this);
280
            }
281
        }
282
283
        var brokenTypes = {};
284
        var brokenProperties = {};
285
286
        for (var key in this.types) {
287
            validate(this, key, brokenTypes, this.types[key]);
288
        }
289
290
        for (var key in this.properties) {
291
            validate(this, key, brokenProperties, this.properties[key]);
292
        }
293
294
        brokenTypes = Object.keys(brokenTypes).filter(function(name) {
295
            return brokenTypes[name];
296
        });
297
        brokenProperties = Object.keys(brokenProperties).filter(function(name) {
298
            return brokenProperties[name];
299
        });
300
301
        if (brokenTypes.length || brokenProperties.length) {
302
            return {
303
                types: brokenTypes,
304
                properties: brokenProperties
305
            };
306
        }
307
308
        return null;
309
    },
310
    dump: function(syntaxAsAst, pretty) {
311
        return {
312
            generic: this.generic,
313
            types: dumpMapSyntax(this.types, !pretty, syntaxAsAst),
314
            properties: dumpMapSyntax(this.properties, !pretty, syntaxAsAst)
315
        };
316
    },
317
    toString: function() {
318
        return JSON.stringify(this.dump());
319
    }
320
};
321
322
module.exports = Lexer;
323